// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/widget/desktop_aura/x11_desktop_handler.h"

#include <X11/Xatom.h>
#include <X11/Xlib.h>

#include "base/message_loop/message_loop.h"
#include "ui/aura/env.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/base/x/x11_foreign_window_manager.h"
#include "ui/base/x/x11_menu_list.h"
#include "ui/base/x/x11_util.h"
#include "ui/events/platform/platform_event_source.h"
#include "ui/gfx/x/x11_error_tracker.h"
#include "ui/views/widget/desktop_aura/desktop_window_tree_host_x11.h"

namespace {

const char* kAtomsToCache[] = {
    "_NET_ACTIVE_WINDOW",
    NULL
};

// Our global instance. Deleted when our Env() is deleted.
views::X11DesktopHandler* g_handler = NULL;

} // namespace

namespace views {

// static
X11DesktopHandler* X11DesktopHandler::get()
{
    if (!g_handler)
        g_handler = new X11DesktopHandler;

    return g_handler;
}

X11DesktopHandler::X11DesktopHandler()
    : xdisplay_(gfx::GetXDisplay())
    , x_root_window_(DefaultRootWindow(xdisplay_))
    , x_active_window_(None)
    , wm_user_time_ms_(0)
    , current_window_(None)
    , current_window_active_state_(NOT_ACTIVE)
    , atom_cache_(xdisplay_, kAtomsToCache)
    , wm_supports_active_window_(false)
{
    if (ui::PlatformEventSource::GetInstance())
        ui::PlatformEventSource::GetInstance()->AddPlatformEventDispatcher(this);
    aura::Env::GetInstance()->AddObserver(this);

    XWindowAttributes attr;
    XGetWindowAttributes(xdisplay_, x_root_window_, &attr);
    XSelectInput(xdisplay_, x_root_window_,
        attr.your_event_mask | PropertyChangeMask | StructureNotifyMask | SubstructureNotifyMask);

    if (ui::GuessWindowManager() == ui::WM_WMII) {
        // wmii says that it supports _NET_ACTIVE_WINDOW but does not.
        // https://code.google.com/p/wmii/issues/detail?id=266
        wm_supports_active_window_ = false;
    } else {
        wm_supports_active_window_ = ui::WmSupportsHint(atom_cache_.GetAtom("_NET_ACTIVE_WINDOW"));
    }
}

X11DesktopHandler::~X11DesktopHandler()
{
    aura::Env::GetInstance()->RemoveObserver(this);
    if (ui::PlatformEventSource::GetInstance())
        ui::PlatformEventSource::GetInstance()->RemovePlatformEventDispatcher(this);
}

void X11DesktopHandler::ActivateWindow(::Window window)
{
    if ((current_window_ == None || current_window_ == window) && current_window_active_state_ == NOT_ACTIVE) {
        // |window| is most likely still active wrt to the X server. Undo the
        // changes made in DeactivateWindow().
        OnActiveWindowChanged(window, ACTIVE);

        // Go through the regular activation path such that calling
        // DeactivateWindow() and ActivateWindow() immediately afterwards results
        // in an active X window.
    }

    if (wm_supports_active_window_) {
        DCHECK_EQ(gfx::GetXDisplay(), xdisplay_);

        // If the window is not already active, send a hint to activate it
        if (x_active_window_ != window) {
            XEvent xclient;
            memset(&xclient, 0, sizeof(xclient));
            xclient.type = ClientMessage;
            xclient.xclient.window = window;
            xclient.xclient.message_type = atom_cache_.GetAtom("_NET_ACTIVE_WINDOW");
            xclient.xclient.format = 32;
            xclient.xclient.data.l[0] = 1; // Specified we are an app.
            xclient.xclient.data.l[1] = wm_user_time_ms_;
            xclient.xclient.data.l[2] = None;
            xclient.xclient.data.l[3] = 0;
            xclient.xclient.data.l[4] = 0;

            XSendEvent(xdisplay_, x_root_window_, False,
                SubstructureRedirectMask | SubstructureNotifyMask,
                &xclient);
        } else {
            OnActiveWindowChanged(window, ACTIVE);
        }
    } else {
        XRaiseWindow(xdisplay_, window);
        // Directly ask the X server to give focus to the window. Note
        // that the call will raise an X error if the window is not
        // mapped.
        XSetInputFocus(xdisplay_, window, RevertToParent, CurrentTime);

        OnActiveWindowChanged(window, ACTIVE);
    }
}

void X11DesktopHandler::DeactivateWindow(::Window window)
{
    if (!IsActiveWindow(window))
        return;

    XLowerWindow(xdisplay_, window);

    // Per ICCCM: http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.7
    // "Clients should not give up the input focus of their own volition.
    // They should ignore input that they receive instead."
    //
    // There is nothing else that we can do. Pretend that we have been
    // deactivated and ignore keyboard input in DesktopWindowTreeHostX11.
    OnActiveWindowChanged(window, NOT_ACTIVE);
}

bool X11DesktopHandler::IsActiveWindow(::Window window) const
{
    return window == current_window_ && current_window_active_state_ == ACTIVE;
}

void X11DesktopHandler::ProcessXEvent(XEvent* event)
{
    // Ignore focus events that are being sent only because the pointer is over
    // our window, even if the input focus is in a different window.
    if (event->xfocus.detail == NotifyPointer)
        return;

    switch (event->type) {
    case FocusIn:
        if (current_window_ != event->xfocus.window)
            OnActiveWindowChanged(event->xfocus.window, ACTIVE);
        break;
    case FocusOut:
        if (current_window_ == event->xfocus.window)
            OnActiveWindowChanged(None, NOT_ACTIVE);
        break;
    default:
        NOTREACHED();
    }
}

bool X11DesktopHandler::CanDispatchEvent(const ui::PlatformEvent& event)
{
    return event->type == CreateNotify || event->type == DestroyNotify || (event->type == PropertyNotify && event->xproperty.window == x_root_window_);
}

uint32_t X11DesktopHandler::DispatchEvent(const ui::PlatformEvent& event)
{
    switch (event->type) {
    case PropertyNotify: {
        // Check for a change to the active window.
        CHECK_EQ(x_root_window_, event->xproperty.window);
        ::Atom active_window_atom = atom_cache_.GetAtom("_NET_ACTIVE_WINDOW");
        if (event->xproperty.atom == active_window_atom) {
            ::Window window;
            if (ui::GetXIDProperty(x_root_window_, "_NET_ACTIVE_WINDOW", &window) && window) {
                x_active_window_ = window;
                OnActiveWindowChanged(window, ACTIVE);
            } else {
                x_active_window_ = None;
            }
        }
        break;
    }

    case CreateNotify:
        OnWindowCreatedOrDestroyed(event->type, event->xcreatewindow.window);
        break;
    case DestroyNotify:
        OnWindowCreatedOrDestroyed(event->type, event->xdestroywindow.window);
        // If the current active window is being destroyed, reset our tracker.
        if (x_active_window_ == event->xdestroywindow.window) {
            x_active_window_ = None;
        }
        break;
    default:
        NOTREACHED();
    }

    return ui::POST_DISPATCH_NONE;
}

void X11DesktopHandler::OnWindowInitialized(aura::Window* window)
{
}

void X11DesktopHandler::OnWillDestroyEnv()
{
    g_handler = NULL;
    delete this;
}

void X11DesktopHandler::OnActiveWindowChanged(::Window xid,
    ActiveState active_state)
{
    if (current_window_ == xid && current_window_active_state_ == active_state)
        return;

    if (current_window_active_state_ == ACTIVE) {
        DesktopWindowTreeHostX11* old_host = views::DesktopWindowTreeHostX11::GetHostForXID(current_window_);
        if (old_host)
            old_host->HandleNativeWidgetActivationChanged(false);
    }

    // Update the current window ID to effectively change the active widget.
    current_window_ = xid;
    current_window_active_state_ = active_state;

    if (active_state == ACTIVE) {
        DesktopWindowTreeHostX11* new_host = views::DesktopWindowTreeHostX11::GetHostForXID(xid);
        if (new_host)
            new_host->HandleNativeWidgetActivationChanged(true);
    }
}

void X11DesktopHandler::OnWindowCreatedOrDestroyed(int event_type,
    XID window)
{
    // Menus created by Chrome can be drag and drop targets. Since they are
    // direct children of the screen root window and have override_redirect
    // we cannot use regular _NET_CLIENT_LIST_STACKING property to find them
    // and use a separate cache to keep track of them.
    // TODO(varkha): Implement caching of all top level X windows and their
    // coordinates and stacking order to eliminate repeated calls to the X server
    // during mouse movement, drag and shaping events.
    if (event_type == CreateNotify) {
        // The window might be destroyed if the message pump did not get a chance to
        // run but we can safely ignore the X error.
        gfx::X11ErrorTracker error_tracker;
        ui::XMenuList::GetInstance()->MaybeRegisterMenu(window);
    } else {
        ui::XMenuList::GetInstance()->MaybeUnregisterMenu(window);
    }

    if (event_type == DestroyNotify) {
        // Notify the XForeignWindowManager that |window| has been destroyed.
        ui::XForeignWindowManager::GetInstance()->OnWindowDestroyed(window);
    }
}

} // namespace views
